/* dl.c - arch-dependent part of loadable module support */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2018  Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/dl.h>
#include <grub/elf.h>
#include <grub/misc.h>
#include <grub/err.h>
#include <grub/mm.h>
#include <grub/i18n.h>
#include <grub/elf.h>

/*
 * Check if EHDR is a valid ELF header.
 */
grub_err_t
grub_arch_dl_check_header (void *ehdr)
{
  Elf_Ehdr *e = ehdr;

  /* Check the magic numbers.  */
  if (e->e_ident[EI_DATA] != ELFDATA2LSB || e->e_machine != EM_SW_64)
    return grub_error (GRUB_ERR_BAD_OS,
		       N_("invalid arch-dependent ELF magic"));

  return GRUB_ERR_NONE;
}

#pragma GCC diagnostic ignored "-Wcast-align"

/* Relocate symbols. */
grub_err_t
grub_arch_dl_relocate_symbols (grub_dl_t mod, void *ehdr,
                               Elf_Shdr *s, grub_dl_segment_t seg)
{
  Elf_Rela *rel, *max;
  grub_uint64_t *gpptr, *gotptr;

  gotptr = (grub_uint64_t *) mod->got;
  gpptr = (grub_uint64_t *) ((grub_uint64_t) mod->got + 0x8000);

  for (rel = (Elf_Rela *) ((char *) ehdr + s->sh_offset),
         max = (Elf_Rela *) ((char *) rel + s->sh_size);
       rel < max;
       rel = (Elf_Rela *) ((char *) rel + s->sh_entsize))
    {
      grub_addr_t addr;
      Elf_Sym *sym;
      grub_uint64_t value;

      if (seg->size < (rel->r_offset & ~3))
              return grub_error (GRUB_ERR_BAD_MODULE,
                              "reloc offset is out of the segment");

      addr = (grub_addr_t) seg->addr + rel->r_offset;
      sym = (Elf_Sym *) ((char *) mod->symtab
                      + mod->symsize * ELF_R_SYM (rel->r_info));

      /* On the PPC the value does not have an explicit
         addend, add it.  */
      value = sym->st_value + rel->r_addend;

      switch (ELF_R_TYPE (rel->r_info))
        {
        case R_SW64_NONE:
          break;
        case R_SW64_REFLONG:
          *(grub_uint32_t *)addr = value;
          break;
        case R_SW64_REFQUAD:
          {
            *(grub_uint32_t *)addr = value;
            *((grub_uint32_t *)addr + 1) = value >> 32;
            break;
          }
        case R_SW64_GPREL32:
          {
            value -= (grub_uint64_t)gpptr;
            if ((grub_int32_t)value != value)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "R_SW64_GPREL32 relocation out of range");
            *(grub_uint32_t *) addr = value;
            break;
          }
        case R_SW64_LITERAL:
          {
            grub_uint64_t li_hi;
            grub_uint64_t li_lo;

            li_hi = (grub_uint64_t) gotptr + (((grub_uint64_t)ELF_R_TYPE(rel->r_info)) >> 8 );
            li_lo = li_hi - ((grub_uint64_t) gpptr);
            if ((grub_int16_t)li_lo != li_lo)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "R_SW64_LITERAL relocation out of range");

            *(grub_uint16_t *) addr = (li_lo & 0xffff);
            *(grub_uint64_t *) (li_hi) = value;
            gotptr++;
            break;
          }
        case R_SW64_LITERAL_GOT:
          break;
        case R_SW64_LITUSE:
          break;
        case R_SW64_GPDISP:
          {
            grub_uint64_t hi;
            grub_uint64_t lo;
            grub_uint64_t gpoffset = (grub_int64_t)gpptr - addr;
            lo = (grub_int16_t) gpoffset;
            hi = (grub_int32_t) (gpoffset - lo);
            if (hi + lo != gpoffset)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "R_SW64_GPDISP relocation out of range");

            if (gpoffset & 0x8000) {
                    hi = ((gpoffset + 0x8000) >> 16) & 0xffff;
                    lo = gpoffset & 0xffff;
            } else {
                    hi = (gpoffset >> 16) & 0xffff;
                    lo = gpoffset & 0xffff;
            }

            *(grub_uint32_t *) addr = ( *(grub_uint32_t *)addr & 0xffff0000) | (hi & 0xffff);
            *(grub_uint32_t *) ((unsigned long)addr + rel->r_addend) =
                    (((*(grub_uint32_t *)((unsigned long)addr + rel->r_addend)) & 0xffff0000) | (lo & 0xffff));
            break;
          }
        case R_SW64_BRADDR:
          {
            grub_uint64_t braddr = (*(grub_uint64_t *)addr) + value - ((grub_uint64_t)addr + 4);
            braddr = (grub_int64_t)braddr >> 2;
            if (braddr + (1<<21) >= 1<<21)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "R_SW64_BRADDR relocation out of range");

            *(grub_uint32_t *) addr = (*(grub_uint32_t *)addr & (~0x1fffff)) | (braddr & 0x1fffff);
            break;
          }
        case R_SW64_HINT:
          break;
        case R_SW64_SREL32:
          {
            value -= (grub_uint64_t)addr;
            if ((grub_int32_t)value != value)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "R_SW64_SREL32 relocation out of range");

            *(grub_uint32_t *) addr = value;
            break;
          }
        case R_SW64_SREL64:
          {
            grub_uint64_t srel64 = *(grub_uint64_t *)addr + value;;
            srel64 -= (grub_uint64_t)addr;
            *(grub_uint64_t *) addr = srel64;
            break;
          }
        case R_SW64_GPRELHIGH:
          {
            grub_uint64_t gprel_hi = (grub_int64_t)(value - (grub_uint64_t)gpptr);

            if (gprel_hi & 0x8000)
                    gprel_hi = (grub_int64_t)(((grub_int64_t)gprel_hi + 0x8000) >> 16);
            else
                    gprel_hi = (grub_int64_t)gprel_hi >> 16;

            if ((grub_int16_t)gprel_hi != gprel_hi)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "GPRELHIGH out of range\n");

            *(grub_uint32_t *) addr = (*(grub_uint32_t *)addr & 0xffff0000) | (gprel_hi & 0xffff);
            break;
          }
        case R_SW64_GPRELLOW:
          {
            grub_uint64_t gprel_lo = value - (grub_uint64_t)gpptr;
            *(grub_uint32_t *) addr = (*(grub_uint32_t *)addr & 0xffff0000) | (gprel_lo & 0xffff);
            break;
          }
        case R_SW64_GPREL16:
          {
            grub_uint64_t gprel16 = (*(grub_uint64_t *)addr + value);
            gprel16 = (grub_uint64_t)(gprel16 - (grub_uint64_t)gpptr);
            if ((grub_int16_t)gprel16 != gprel16)
                    return grub_error (GRUB_ERR_BAD_MODULE,
                                    "R_SW64_GPREL16 relocation out of range");
            *(grub_uint16_t *) addr = gprel16;
            break;
          }
        default:
          return grub_error (GRUB_ERR_NOT_IMPLEMENTED_YET,
                          N_("relocation 0x%lx is not implemented yet"),
                          ELF_R_TYPE (rel->r_info));
        }
    }

  return GRUB_ERR_NONE;
}
